package starblaze 
{
  import flash.display.InteractiveObject;
  import flash.utils.ByteArray;
  import starblaze.Action;
  import starblaze.Initializer;
	
  public final class Simulation 
  {
    
    public function Simulation(maxNumParticles:uint, outputStreamFormat:StreamFormat, internalStreamFormat:StreamFormat)
    {
      this.maxNumParticles = maxNumParticles;
      this.outputStreamFormat = outputStreamFormat;
      this.internalStreamFormat = internalStreamFormat;
    }
    
    private var m_streamDirty:Boolean = true;
    private var m_bytecodeDirty:Boolean = true;
    
    private var m_maxNumParticles:uint = 0;
    public function get maxNumParticles():uint { return m_maxNumParticles; }
    public function set maxNumParticles(num:uint):void
    {
      m_maxNumParticles = num;
      m_streamDirty = true;
    }
    
    private var m_numParticles:uint = 0;
    public function get numParticles():uint { return m_numParticles; }
    
    private var m_outputStreamFormat:StreamFormat = null;
    public function get outputStreamFormat():StreamFormat { return m_outputStreamFormat; }
    public function set outputStreamFormat(format:StreamFormat):void
    {
      m_outputStreamFormat = format;
      m_streamDirty = true;
    }
    
    private var m_internalStreamFormat:StreamFormat = null;
    public function get internalStreamFormat():StreamFormat { return m_internalStreamFormat; }
    public function set internalStreamFormat(format:StreamFormat):void
    {
      m_internalStreamFormat = format;
      m_streamDirty = true;
    }
    
    private const m_actions:Vector.<Action> = new Vector.<Action>();
    public function addAction(action:Action):Simulation
    {
      m_actions.push(action);
      m_bytecodeDirty = true;
      return this;
    }
    
    private const m_initializers:Vector.<Initializer> = new Vector.<Initializer>();
    public function addInitializer(initializer:Initializer):Simulation
    {
      m_initializers.push(initializer);
      m_bytecodeDirty = true;
      return this;
    }
    
    public function finalize():void
    {
      if (m_streamDirty)
        buildStreams();
        
      if (m_bytecodeDirty)
        buildBytecode();
        
      if (m_streamDirty || m_bytecodeDirty)
        m_numParticles = 0;
    }
    
    private var m_outputStream   :ByteArray = null;
    private var m_internalStream :ByteArray = null;
    private var m_deathFlagStream:ByteArray = null;
    private function buildStreams():void
    {
      m_outputStream = new ByteArray();
      m_outputStream.length = m_maxNumParticles * m_outputStreamFormat.numBytes;
      
      m_internalStream = new ByteArray();
      m_internalStream.length = m_maxNumParticles * m_internalStreamFormat.numBytes;
      
      m_deathFlagStream = new ByteArray();
      m_deathFlagStream.length = m_maxNumParticles; // all bools
      m_deathFlagStream.position = 0;
      for (var i:uint = 0; i < m_deathFlagStream.length; ++i)
        m_deathFlagStream.writeBoolean(true);
      
      m_streamDirty = false;
    }
    
    private var m_initializerBytecode:ByteArray      = null;
    private var m_actionBytecode     :ByteArray      = null;
    private var m_constantStream     :ByteArray      = null;
    private var m_variableStream     :ByteArray      = null;
    private var m_constantLookup     :ConstantLookup = null;
    private var m_variableLookup     :VariableLookup = null;
    private function buildBytecode():void
    {
      const oal:OutputAttributeLookup = new OutputAttributeLookup(m_outputStreamFormat.m_attributes);
      const ial:InternalAttributeLookup = new InternalAttributeLookup(m_internalStreamFormat.m_attributes);
      const av:AttributeVerifer = new AttributeVerifer(m_outputStreamFormat, m_internalStreamFormat);
      
      var builder:BytecodeBuilder;
      var cr:ConstantRegistrar;
      var vr:VariableRegistrar;
      var cl:ConstantLookup;
      var vl:VariableLookup;
      var v:Variable;
      var c:Constant;
      
      m_constantStream = new ByteArray();
      m_variableStream = new ByteArray();
      m_constantLookup = new ConstantLookup();
      m_variableLookup = new VariableLookup();
      
      // build initialzer bytecode
      builder = new BytecodeBuilder();
      for each (var initializer:Initializer in m_initializers)
      {
        // verify needed attributes
        initializer.needAttributes(av);
        
        // register constants & variables
        cr = new ConstantRegistrar();
        vr = new VariableRegistrar();
        initializer.registerConstants(cr);
        initializer.registerVariables(vr);
        initializer.m_constantRegistrar = cr;
        
        // constants are global to all components
        m_constantStream.length += cr.numBytes;
        for each (c in cr.m_registry)
          m_constantLookup.m_lut.push(c);
          
        // variables are local to individual components
        m_variableStream.length = Math.max(m_variableStream.length, vr.numBytes);
        m_variableLookup.m_lut = vr.m_registry;
        
        // build bytecode
        initializer.buildBytecode(builder, cl, vl, oal, ial);
      }
      m_initializerBytecode = builder.bytecode;
      // END build initializer bytecode
      
      // build initialzer bytecode
      builder = new BytecodeBuilder();
      for each (var action:Action in m_actions)
      {
        // verify needed attributes
        action.needAttributes(av);
        
        // register constants & variables
        cr = new ConstantRegistrar();
        vr = new VariableRegistrar();
        action.registerConstants(cr);
        action.registerVariables(vr);
        action.m_constantRegistrar = cr;
        
        // constants are global to all components
        m_constantStream.length += cr.numBytes;
        for each (c in cr.m_registry)
          m_constantLookup.m_lut.push(c);
          
        // variables are local to individual components
        m_variableStream.length = Math.max(m_variableStream.length, vr.numBytes);
        m_variableLookup.m_lut = vr.m_registry;
        
        // build bytecode
        action.buildBytecode(builder, cl, vl, oal, ial);
      }
      m_actionBytecode = builder.bytecode;
      // END: build action bytecode
      
      // lower dirty flag
      m_bytecodeDirty = false;
    }
    
    private var m_numNewParticles:uint = 0;
    private var m_numNewParticlesFraction:Number = 0.0;
    private var m_emissionRate:Number = 0;
    public function get emissionRate():Number { return m_emissionRate; }
    public function set emissionRate(rate:Number):void { m_emissionRate = rate; }
    public function spawnParticles(numParticles:uint):void
    {
      m_numNewParticles += numParticles;
    }
    
    public function render(renderer:IRenderer):void
    {
      renderer.render(m_outputStream, m_outputStreamFormat.numBytes, m_numParticles);
    }
    
    private var m_interpreter:Interpreter = new Interpreter();
    public function step(dt:Number):void
    {
      const outputStreamStride   :uint = m_outputStreamFormat.numBytes;
      const internalStreamStride :uint = m_internalStreamFormat.numBytes;
      var i:uint;
      
      // safety check
      finalize();
      
      // finalize number of new particles
      m_numNewParticlesFraction += m_emissionRate * dt;
      m_numNewParticles += uint(m_numNewParticlesFraction);
      m_numNewParticles = Math.min(m_numNewParticles, m_maxNumParticles - m_numParticles);
      m_numNewParticlesFraction = m_numNewParticlesFraction % 1.0;
      
      // update constants
      for each (var initializer:Initializer in m_initializers)
        initializer.updateConstants(m_constantStream, m_constantLookup);
      for each (var action:Action in m_actions)
        action.updateConstants(m_constantStream, m_constantLookup);
        
      // spawn new particles & run initializer bytecode
      i = 0;
      m_deathFlagStream.position = 0;
      while (i < m_maxNumParticles && m_numNewParticles > 0)
      {
        if (m_deathFlagStream.readBoolean())
        {
          m_interpreter.run(m_initializerBytecode   , 
                            m_outputStream          , 
                            m_internalStream        , 
                            m_deathFlagStream       , 
                            m_constantStream        , 
                            m_variableStream        , 
                            i * outputStreamStride  , 
                            i * internalStreamStride, 
                            i                       );
        }
        --m_numNewParticles;
        ++i;
      }
      m_numNewParticles = 0;
      // END: spawn new particles & run initializer bytecode
      
      // run action bytecode
      i = 0;
      m_deathFlagStream.position = 0;
      while (i < m_maxNumParticles)
      {
        if (!m_deathFlagStream.readBoolean())
        {
          m_interpreter.run(m_actionBytecode        , 
                            m_outputStream          , 
                            m_internalStream        , 
                            m_deathFlagStream       , 
                            m_constantStream        , 
                            m_variableStream        , 
                            i * outputStreamStride  , 
                            i * internalStreamStride, 
                            i                       );
        }
        ++i;
      }
      // END: run action bytecode
    }
    
  }

}
